iOS渐入佳境之响应链Responder Chain

前言

初学 Cocoa/Objective-C 时,如果有编程语言的基础,那么仅仅Objective-C这门语言很容易在几天就熟悉,然后就会花费大部分的时间学习 Cocoa 框架和适应它是如何工作的,尽管可能会开发出来像样的App,但毕竟如果只是处于能熟悉掌握API的使用这种level,如果想要估计做好iOS开发,或者说是真正的深刻的掌握OC这门语言,那我接下来的渐入佳境系列可能会帮助到你。

正文

这篇主要介绍响应链Responder Chain,在理解之前有必要先说明几个概念:responder对象,事件event,nextResponder。

一、responder对象:

在iOS系统中,能够响应并处理事件的对象称之为responder object, UIResponder是所有responder对象的基类。

UIApplication,UIViewController,UIView和所有继承自UIView的UIKit类(包括UIWindow,继承自UIView)都直接或间接的继承自UIResponder,所以它们的实例都是responder object对象。

二、事件event

在UIResponder类中定义了处理各种事件,包括触摸事件(Touch Event)、运动事件(Motion Event)和远程控制事件(Remote-Control Events)的编程接口。

触摸事件

1
2
3
4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

这四个方法分别处理触摸开始事件,触摸移动事件,触摸终止事件,以及触摸跟踪取消事件。

加速计事件

1
2
3
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

当用户以特定方式移动设备,比如摇摆设备时,iPhone或者iPod touch会产生运动事件。运动事件源自设备加速计。系统会对加速计的数据进行计算,如果符合某种模式,就将它解释为手势,然后创建一个代表该手势的UIEvent对象,并发送给当前活动的应用程序进行处理。

请注意:在iPhone 3.0上,只有摇摆设备的动作会被解释为手势,并形成运动事件。
运动事件比触摸事件简单得多。系统只是告诉应用程序动作何时开始及何时结束,而不包括在这个过程中发生的每个动作的时间。而且,触摸事件中包含一个触摸对象的集合及其相关的状态,而运动事件中除了事件类型、子类型、和时间戳之外,没有其它状态。系统以这种方式来解析运动手势,避免和方向变化事件造成冲突。

为了处理运动事件,UIResponder的子类必须实现motionBegan:withEvent:或motionEnded:withEvent:方法之一,或者同时实现这两个方法。举例来说,如果用户希望赋以水平摆动和垂直摆动不同的意义,就可以在motionBegan:withEvent:方法中将当前加速计轴的值缓存起来,并将它们和motionEnded:withEvent:消息传入的值相比较,然后根据不同的结果进行动作。响应者还应该实现
motionCancelled:withEvent:方法,以便响应系统发出的运动取消的事件。有些时候,这些事件会告诉您整个动作根本不是一个正当的手势。

应用程序及其键盘焦点窗口会将运动事件传递给窗口的第一响应者。如果第一响应者不能处理,事件就沿着响应者链进行传递,直到最终被处理或忽略,这和触摸事件的处理相类似(详细信息请参见“事件的传递”部分)。但是,摆动事件和触摸事件有一个很大的不同,当用户开始摆动设备时,系统就会通过motionBegan:withEvent:消息的方式向第一响应者发送一个运动事件,如果第一响应者不能处理,该事件就在响应者链中传递;如果摆动持续的时间小于1秒左右,系统就会向第一响应者发送motionEnded:withEvent:消息;但是,如果摆动时间持续更长,如果系统确定当前的动作不是摆动,则第一响应者会收到一个motionCancelled:withEvent:消息。
如果摆动事件沿着响应者链传递到窗口而没有被处理,且UIApplication的applicationSupportsShakeToEdit属性被设置为YES,则iPhone OS会显示一个带有撤消(Undo)和重做(Redo)的命令。缺省情况下,这个属性的值为NO。

远程控制事件

1
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

它能够被外部配件引发。

UITouch & UIEvent

在屏幕上的每一次动作事件都是一次Touch,在iOS中用UITouch对象表示每一次的触控,多个Touch组成一次Event,用UIEvent来表示一次事件对象。

三、nextResponder:

UIResponder中的默认实现是什么都不做,但UIKit中UIResponder的直接子类(UIView,UIViewController…)的默认实现是将事件沿着responder chain继续向上传递到下一个responder,即nextResponder。所以在定制UIView子类的上述事件处理方法时,如果需要将事件传递给next responder,可以直接调用super的对应事件处理方法,super的对应方法将事件传递给next responder,即使用

[super touchesBegan:touches withEvent:event];

不建议直接向nextResponder发送消息,这样可能会漏掉父类对这一事件的其他处理。

[self.nextResponder touchesBegan:touches withEvent:event];

另外,在定制UIView子类的事件处理方法时,如果其中一个方法没有调用super的对应方法,则其他方法也需要重写,不使用super的方法,否则事件处理流程会很混乱。

巧妙利用nextResponder

通过UIViewController的view属性可以访问到其管理的view对象,及此view的所有subviews。但是根据一个view对象,没有直接的方法可以得到管理它的viewController,但我们使用responder chain可以间接的得到,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation UIView (ParentController)
-(UIViewController*)parentController{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end

四、responder chain:

responder chain是一系列连接的responder对象,通过responder对象可以将处理事件的责任传递给下一个,更高级的对象,即当前responder对象的nextResponder。

iOS中responder chain的结构为:

从图中可以看出:

  • UIView的nextResponder属性,如果有管理此view的UIViewController对象,则为此UIViewController对象;否则nextResponder即为其superview。
  • UIViewController的nextResponder属性为其管理view的superview.
  • UIWindow的nextResponder属性为UIApplication对象。
  • UIApplication的nextResponder属性为nil。

事件处理过程:

第一步:当有事件发生时,先进行事件传递

如上图,iOS中事件传递首先从App(UIApplication)开始,接着传递到Window(UIWindow),在接着往下传递到View(这个View就是hit-test view,至于如何找到,下面会仔细讲解这部分)之前,Window会将事件交给手势识别器GestureRecognizer。

如果在此期间,GestureRecognizer识别了传递过来的事件,则该事件将不会继续传递到View去,而是像我们之前说的那样交给Target(ViewController)进行处理。

第二步:寻找响应者

第一响应者是第一个接收事件的View对象,我们在Xcode的Interface Builder画视图时,可以看到视图结构中就有First Responder。

当iOS识别出一个事件,它将传递这个事件给看起来与处理事件最相关的初始对象,例如触摸发生的view。如果初始对象不能够处理这个事件,iOS会继续传递这个事件给更大范围的对象,直到找到一个拥有足够的条件环境(context)来处理这个事件的对象。这一系列的对象被称为一个响应链(responder chain),并且,当iOS在链上传递事件时,它同时转移响应这个事件的职责。这个设计模式让事件处理具备协作性和动态特性。

五、hit-test view

hit-test view从表面意思可以看出就是接受点击测试的视图。在发生一个点击事件的时候,UIApplication对象会从队列的顶部获取到这个事件,在把这个事件分配给可以处理这个事件的视图。 事件会由一个指定的途径传递,一直到事件可以响应,如果响应链走了一遍,没有发现可以接收事件的视图,那么该事件就会被丢弃。如果有视图可以响应这个事件,这个视图就是hit-test view,发现hit-test view的这个过程称为hit-testing.

接下来来说下当一个touch事件发生后,hit-testing是如果检测到哪个视图响应的。

hit-testing在检测hit-test view的时候,会遍历view的所有子视图来判断是哪个视图包含当前的点击点。一旦检测到hit-test view,就会传递touch事件给视图去处理。

在上面的图片中,如果touch点击的时候,是点击在view E里面。则检测过程是
1.首先是touch点在view A的bounds中,接下来检测view B和view C.
2.检测到touch点不在view B,在view C中,接下来检测view D和view E.
3.检测到touch点不在view D中,在view E中。
此时hit-testing过程已经完成,view E就是hit-test View.

检测到hit-test View后,此view拥有优先处理事件的权利,如果view不能处理这个事件,事件就会沿着响应者链向下传递,直到找到一个能处理该事件的view.
那这个传递过程是怎么样的呢,我们就要知道The Responder Chain的组成了,在开头的时候已经说了(个人理解)

注意:hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。

六、UIView的bounds到底是干嘛的?

UIView的bounds到底是干嘛的?
这篇博文讲的很详细!

frame : 当前 view 在其 superView 中的位置及大小

bounds : 是 view 自身的坐标系(为其 subViews 提供的坐标系)

center : 该view的中心点在父view坐标系统中的位置

参考:官方文档:iOS事件处理指南(译)

   Responder Chain简析

   The Responder Chain(响应链)